package tomkitty.util;

import tomkitty.turdlet.*;
import tomkitty.util.*;

import java.lang.ClassLoader;
import java.io.*;
import java.util.Hashtable;
import java.util.jar.JarInputStream;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 *
 * Dynamically loads classes from JAR files.
 *
 * java.lang.ClassLoader lets one load new classes into the runtime class 
 * path dynamically. Class file contents are loaded as a byte array with 
 * defineClass, and then linked with resolveClass.  AppLoader, given either 
 * a path to a directory of jar files or a single jar file, scans the jar 
 * for .class JarEntries and reads their contents into a byte array. This 
 * byte array is then passed to defineClass and resolveClass.  defineClass 
 * returns a Class instance.
 * <p>
 * After the Class has been added to the classpath, it is tested whether or 
 * not it implements Turdlet.  If it does, init() is run.
 * <p>
 * Whether or not it implements Turdlet, it is added to the list of loaded 
 * Classes, "loadedClasses"
 *
 * @author alan
 *
 */
public class AppLoader extends ClassLoader {
	
	// list of classes that have been loaded: their names, and their classes
	// Name format: [appname]/[classname]
	// ie: "FooApp/FooClass"

	private static Hashtable<String, Class> loadedClasses = 
		new Hashtable<String, Class>();
	
	private JarFile currentJar;
	
	/**
	 * Given a path, finds application jars.
	 * The jars are then passed to loadJar, which loads the classes inside.
	 *
	 * @param appDirPath Path to application directory, within which 
	 * 				application jars can be found.
	 *
	 * @return Whether the directory was succesfully scanned.
	 */
	public boolean scanDir(String appDirPath) {
		File scanDir = new File(appDirPath);
		
		// isn't a directory or doesn't exist, so fail

		if(!scanDir.isDirectory() || !scanDir.exists()) {
			return false;
		}
		
		// get a list of jar files in the directory, using an anonymous inner 
		// FileFilter

		File[] files = scanDir.listFiles(
			new FileFilter() {

					public boolean accept(File scanDir) {

						String name = null;
						if(scanDir != null) {
							name = scanDir.getName();
						}
				
						return name.endsWith("jar");
					}
			}
		);
	    
		// for each jar file, try and load it up

		for(int i = 0; i < files.length; i++) {
			try {
				//System.out.println("Trying to load "+files[i].getName());
				currentJar = new JarFile(files[i]);
				if(!loadFromStream(new JarInputStream(new FileInputStream(files[i])))) {
					return false;
				}
			} catch (FileNotFoundException fnf) {
				fnf.printStackTrace();
				return false;
			} catch (IOException ioe) {
				ioe.printStackTrace();
				return false;
			}
		}
		
		return true;
	}
	
	public boolean loadFromStream(JarInputStream jIs) {
		
		try {
			for(JarEntry jEntry; (jEntry = jIs.getNextJarEntry()) != null; 
					jIs.closeEntry()) {
				if(jEntry.isDirectory()) {
					//System.out.println(jEntry.getName()+" is a directory, scanning it.");
					loadFromStream(new JarInputStream(currentJar.getInputStream(jEntry)));
				} else {
					//System.out.println(jEntry.getName()
					//		+ " isn't a directory, attempting to load it.");

					String name = jEntry.getName();

					if(name.endsWith(".class")) {

						// It's a class, let's load it up

						InputStream in = new BufferedInputStream(
								currentJar.getInputStream(jEntry));
						ByteArrayOutputStream baOs = new ByteArrayOutputStream();
						
						int read;
						while((read = in.read()) != -1)	{
							baOs.write(read);
						}
						
						Class tmpClass = 
							defineClass(null, baOs.toByteArray(), 0, baOs.size());
						resolveClass(tmpClass);
				
						String fullName = tmpClass.getName(); //bob.wtf.test.Test...
						String simplifiedName = 
							fullName.substring(fullName.lastIndexOf('.')+1);

						//System.out.println("Class simplified name: "+simplifiedName);

						String appName = currentJar.getName().substring(currentJar.getName().lastIndexOf(File.separator)+1, currentJar.getName().indexOf(".jar"));

						//System.out.println("App name (jar name): "+appName);

						String hashKey = "/"+appName+"/"+simplifiedName;
						loadedClasses.put(hashKey, tmpClass);

						//System.out.println("Added '"+hashKey+"'");
					}
				}
			}
		} catch (IOException ioe) {
			ioe.printStackTrace();
			return false;
		}

		return true;
	}

	/**
	 * Gets a turdlet by "name". (The "name" here is actually
	 * more like a resource identifier. It is of the form 
	 * '/[<i>appname</i>]/[<i>turdletname</i>]', where <i>appname</i> is the
	 * name of the app jar file with the .jar extension removed.) If the 
	 * turdlet has not been loaded then a {@link ClassNotFoundException} is
	 * thrown. 
	 * <p>
	 * Also, since the path part of a URI may have more than two
	 * segments, or it may have a '/' appended to the end (or it may not),
	 * this method only looks at the first two segments of the <i>name</i>
	 * parameter. This is to simplify the code calling this method.
	 *
	 * @param name The name of the turdlet to load
	 */
	public static Turdlet getTurdlet(String name) throws ClassNotFoundException,
			InstantiationException, IllegalAccessException {

		Class ret = null;

		String[] segments = name.split("/");
		if (segments.length >= 3) {
			ret = loadedClasses.get("/" + segments[1] + "/" + segments[2]);
		}

		if (ret == null) {
			throw new ClassNotFoundException();
		}

		return (Turdlet) ret.newInstance();
	}
}
